# A dictionary of movie critics and their ratings of a small set of movies

critics={'Lisa Rose': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.5,
 'Just My Luck': 3.0, 'Superman Returns': 3.5, 'You, Me and Dupree': 2.5, 
 'The Night Listener': 3.0},
'Gene Seymour': {'Lady in the Water': 3.0, 'Snakes on a Plane': 3.5, 
 'Just My Luck': 1.5, 'Superman Returns': 5.0, 'The Night Listener': 3.0, 
 'You, Me and Dupree': 3.5}, 
'Michael Phillips': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.0,
 'Superman Returns': 3.5, 'The Night Listener': 4.0},
'Claudia Puig': {'Snakes on a Plane': 3.5, 'Just My Luck': 3.0,
 'The Night Listener': 4.5, 'Superman Returns': 4.0, 
 'You, Me and Dupree': 2.5},
'Mick LaSalle': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, 
 'Just My Luck': 2.0, 'Superman Returns': 3.0, 'The Night Listener': 3.0,
 'You, Me and Dupree': 2.0}, 
'Jack Matthews': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0,
 'The Night Listener': 3.0, 'Superman Returns': 5.0, 'You, Me and Dupree': 3.5},
'Toby': {'Snakes on a Plane':4.5,'You, Me and Dupree':1.0,'Superman Returns':4.0}}

from math import sqrt

def extractSharedItems(prefs, person1, person2):
    sharedItems = {}
    for item in prefs[person1]:
        if item in prefs[person2]:
            sharedItems[item] = 1
    return sharedItems

def calcEuclideanDistance( prefs, person1, person2 ):
    return sim_distance( prefs, person1, person2 )

# Returns a distance-based similarity score for person1 and person2
def sim_distance(prefs,person1,person2):
  # Get the list of shared items
  sharedItems = extractSharedItems(prefs, person1, person2)

  # if they have no ratings in common, return 0
  if len(sharedItems) == 0:
      return 0

  # Add up the squares of all the differences
  sumOfSquares = sum( [ pow( prefs[person1][item] - prefs[person2][item],2)
                       for item in prefs[person1] if item in prefs[person2]] )

  euclideanDistance = 1 / (1 + sqrt( sumOfSquares ) )
  return euclideanDistance

def calcPearsonCorrelation( prefs, person1, person2 ):
    return sim_pearson( prefs, person1, person2 )

# Returns the Pearson correlation coefficient for p1 and p2
def sim_pearson(prefs,person1,person2):
  # Get the list of shared items
  sharedItems = extractSharedItems(prefs, person1, person2)

  # if they are no ratings in common, return 0
  if not sharedItems:
      return 0
  # if len(sharedItems) == 0:
  #    return 0

  # Sum calculations
  n = len(sharedItems)
  
  # Sums of all the preferences
  sum1 = sum([prefs[person1][it] for it in sharedItems])
  sum2 = sum([prefs[person2][it] for it in sharedItems])
  
  # Sums of the squares
  sum1Sq = sum([pow(prefs[person1][it],2) for it in sharedItems])
  sum2Sq = sum([pow(prefs[person2][it],2) for it in sharedItems])	
  
  # Sum of the products
  pSum = sum([prefs[person1][it] * prefs[person2][it] for it in sharedItems])
  
  # Calculate r (Pearson score)
  num = pSum - (sum1 * sum2 / n)
  den = sqrt((sum1Sq - pow(sum1,2) / n) * (sum2Sq - pow(sum2,2) / n))
  if den == 0:
      return 0

  pearsonCorrelation = num / den
  return pearsonCorrelation

# Returns the best matches for person from the prefs dictionary.
# Number of results and similarity function are optional params.
def topMatches(prefs, person, n=5, similarity=calcPearsonCorrelation):
  scores=[ (similarity(prefs, person, other), other)
          for other in prefs if other != person ]
  scores.sort()
  scores.reverse()
  return scores[0:n]

# Gets recommendations for a person by using a weighted average
# of every other user's rankings
def getRecommendations(prefs, person, similarity=calcPearsonCorrelation):
  totals = {}
  simSums = {}
  for other in prefs:
    # don't compare me to myself
    if other == person:
        continue
    sim = similarity(prefs, person, other)

    # ignore scores of zero or lower
    if sim <= 0: continue
    for item in prefs[other]:
      # only score movies I haven't seen yet
      if item not in prefs[person] or prefs[person][item] == 0:
        # Similarity * Score
        totals.setdefault(item, 0)
        totals[item] += prefs[other][item] * sim
        # Sum of similarities
        simSums.setdefault(item, 0)
        simSums[item] += sim

  # Create the normalized list
  rankings=[ (total / simSums[item], item) for item, total in totals.items() ]

  # Return the sorted list
  rankings.sort()
  rankings.reverse()
  return rankings

def transformPrefs(prefs):
  result={}
  for person in prefs:
    for item in prefs[person]:
      result.setdefault(item,{})
      
      # Flip item and person
      result[item][person] = prefs[person][item]
  return result

def calculateSimilarItems(prefs,n=10):
  # Create a dictionary of items showing which other items they
  # are most similar to.
  result={}
  # Invert the preference matrix to be item-centric
  itemPrefs = transformPrefs(prefs)
  c = 0
  for item in itemPrefs:
    # Status updates for large datasets
    c += 1
    if c % 100 == 0:
        print "%d / %d" % (c,len(itemPrefs))

    # Find the most similar items to this one
    scores = topMatches(itemPrefs, item, n=n, similarity=sim_distance)
    result[item] = scores
  return result

def getRecommendedItems(prefs,itemMatch,user):
  userRatings = prefs[user]
  scores={}
  totalSim={}
  # Loop over items rated by this user
  for (item,rating) in userRatings.items( ):

    # Loop over items similar to this one
    for (similarity,item2) in itemMatch[item]:

      # Ignore if this user has already rated this item
      if item2 in userRatings:
          continue
      # Weighted sum of rating times similarity
      scores.setdefault(item2, 0)
      scores[item2] += similarity*rating
      # Sum of all the similarities
      totalSim.setdefault(item2, 0)
      totalSim[item2] += similarity

  # Divide each total score by total weighting to get an average
  rankings=[ (score / totalSim[item], item) for item, score in scores.items( ) ]

  # Return the rankings from highest to lowest
  rankings.sort( )
  rankings.reverse( )
  return rankings

def loadMovieLens(path='./data/movielens'):
  # Get movie titles
  movies={}
  for line in open(path+'/u.item'):
    (id,title)=line.split('|')[0:2]
    movies[id]=title
  
  # Load data
  prefs={}
  for line in open(path+'/u.data'):
    (user,movieid,rating,ts)=line.split('\t')
    prefs.setdefault(user,{})
    prefs[user][movies[movieid]]=float(rating)
  return prefs
